package cbbx_sm.probabilistic_model;

import java.io.Serializable;
import java.text.DecimalFormat;
import java.util.List;
import java.util.Vector;

import cbbx_sm.parser.BoundingBox;


/**
 * A simple bounding box cluster. I suppose a cluster to have a 
 * center, that is the average of all bounding boxes it contains.
 * 
 * @author Alessio Della Motta - University of California, Irvine
 *
 */
public class Cluster implements Serializable {
	private static final long serialVersionUID = 1L;
	
	private String cameraId;
	private double x;
	private double y;
	private double radius = 0.0;
	private String uniqueId = null;
	private List<BoundingBox> elements = new Vector<BoundingBox>();

	public Cluster(String cameraId){
		this.setCameraId(cameraId);
	}
	
	/**
	 * Adding a new element to the cluster and recomputing
	 * its center.
	 */
	public synchronized void addElement(BoundingBox b){
		if (b != null && !elements.contains(b)){
			double new_x = b.getCenterX();
			double new_y = b.getCenterY();
			
			if (elements.size() == 0){
				x = new_x;
				y = new_y;
			}
			else {
				int old_num_elem = elements.size();
				
				x = ((x * old_num_elem) + new_x) / (old_num_elem + 1);
				y = ((y * old_num_elem) + new_y) / (old_num_elem + 1);
			}
			
			elements.add(b);
			
			// Updating radius
			double distance = getClusterDistance(b);
			if (distance > radius) radius = distance;
		}
	}
	
	public synchronized void addElementWithoutUpdatingCenter(BoundingBox b){
		if (b != null && !elements.contains(b)){
			elements.add(b);
			
			// Updating radius
			double distance = getClusterDistance(b);
			if (distance > radius) radius = distance;
		}
	}
	
	
	/**
	 * removing an element from the cluster and recomputing
	 * its center.
	 * TESTME for radius.
	 */
	public synchronized void removeElement(BoundingBox b){
		if (b != null && elements.contains(b)){
			int old_num_elem = elements.size();
			double old_x = b.getCenterX();
			double old_y = b.getCenterY();
			
	
			x = ((x * old_num_elem) - old_x) / (old_num_elem - 1);
			y = ((y * old_num_elem) - old_y) / (old_num_elem - 1);
			
			elements.remove(b);
			
			// Updating radius
			double distance = getClusterDistance(b);
			if (distance == radius){
				radius = 0.0;
				for (BoundingBox box : elements){
					distance = getClusterDistance(box);
					if (distance > radius) radius = distance;
				}
			}
		}
	}
	
	public synchronized void removeElementWithoutUpdatingCenter(BoundingBox b){
		if (b != null && elements.contains(b)){
			elements.remove(b);
			
			// Updating radius
			double distance = getClusterDistance(b);
			if (distance == radius){
				radius = 0.0;
				for (BoundingBox box : elements){
					distance = getClusterDistance(box);
					if (distance > radius) radius = distance;
				}
			}
		}
	}
	
	/**
	 * Return the ditance of a point of coordinates (_x, _y) to
	 * the center of the cluster.
	 * 
	 * @param _x - point x coord
	 * @param _y - point y coord
	 * @return distance from the cluster center
	 */
	public double getClusterDistance(double _x, double _y){
		// Chebyshev distance (infinity-norm)
//		double x_dist = Math.abs(_x - x);
//		double y_dist = Math.abs(_y - y);
//		
//		return x_dist > y_dist ? x_dist : y_dist;
		
		// Euclidean (2-norm)
		return Math.sqrt(
				Math.pow(_x - x, 2)
				+
				Math.pow(_y - y, 2)
			   );
	}
	
	public double getClusterDistance(BoundingBox b){
		return getClusterDistance(b.getCenterX(), b.getCenterY());
	}
	
	public double getClusterDistance(Cluster c){
		return getClusterDistance(c.x, c.y);
	}
	
	/**
	 * Controls if an instance of abounding box is in this
	 * cluster. Trivial: 
	 * Two identical bounding boxes but different instances
	 * are different for this method.
	 * 
	 * @param box
	 * @return
	 */
	public boolean contains(BoundingBox box){
		for (BoundingBox b : elements)
			if (b == box) return true;
		
		return false;
	}
	
	public boolean shouldContain(BoundingBox box){
		if (getClusterDistance(box) <= radius)
			return true;
		return false;
	}
	
	public boolean shouldContain(double _x, double _y){
		if (getClusterDistance(_x, _y) <= radius)
			return true;
		return false;
	}
	
	public int size(){
		return elements.size();
	}
	
	public BoundingBox getElement(int index){
		return elements.get(index);
	}

	/*
	 * Getters & Setters
	 */
	public void setY(double y) {
		this.y = y;
	}
	
	public double getY() {
		return y;
	}
	
	public void setX(double x) {
		this.x = x;
	}
	
	public double getX() {
		return x;
	}
	
	public void setRadius(double radius) {
		this.radius = radius;
	}

	public double getRadius() {
		return radius;
	}

	public void setCameraId(String cameraId) {
		this.cameraId = cameraId;
	}

	public String getCameraId() {
		return cameraId;
	}
	
	@Override
	public String toString(){
		return "CLUSTER: "+cameraId+" (" + x + ", " + y + ")" + " radius = " + radius;
	}
	
	public String getUniqueId(){
		// Was changed to fix a bug where the cluster is not found due to a numeric error in double.
		// the difference was a round off done by double at the 13th digit after the point. 
		if (uniqueId==null){
			DecimalFormat twoDForm = new DecimalFormat("#.####");
			uniqueId = "(" + cameraId + ", " + twoDForm.format(x) + ", " + twoDForm.format(y) + ")";
		}
		return uniqueId;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((getUniqueId() == null) ? 0 : getUniqueId().hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		Cluster cluster = (Cluster) obj;
		return (cluster.getUniqueId().compareTo(getUniqueId())==0);
	}


	

	
}
